﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Reflection;
using Microsoft.Practices.Unity;
using NHibernate;
using NHibernate.Criterion;
using NHibernate.Mapping.Attributes;
using NHibernate.Stat;
using CacheMode = NHibernate.CacheMode;
using FlushMode = NHibernate.FlushMode;

namespace RG.Common.Provider
{
    public static class IDAOSession_Ext
    {
        /// <summary>
        /// Добавляет к абстрактной IDAOSession доступ к конкретной NHibernate-сессии
        /// todo: напрямую внести в IDAOSession?
        /// </summary>
        public static ISession GetSession(this IDAOSession s)
        {
            if (!(s is DAOSession)) throw new ApplicationException("Переданная IDAOSession не является NHibernate-сессией");
            return ((DAOSession)s).Session;
        }
    }

    public class DAOSession : IDAOSession
    {
        protected readonly ISession session;

        public ISession Session { get { return session; } }

        public DAOSession(ISession session, IUnityContainer parentCfg)
        {
            this.session = session;
            ParentCfg = parentCfg; 
        }

        public IUnityContainer ParentCfg { get; set; }

        public IList<T> List<T>(ICriterion expr)
        {
            ICriteria q = session.CreateCriteria(typeof(T));
            if (expr != null)
                q.Add(expr);
            return q.List<T>();
        }

        public IList<T> List<T>(DetachedCriteria dc)
        {
            return dc.GetExecutableCriteria(session).List<T>();
        }

        public T Get<T>(object objId)
        {
            return session.Get<T>(objId);
        }

        public IQuery GetNamedQuery(string queryName)
        {
            return session.GetNamedQuery(queryName).
                SetReadOnly(true).
                SetCacheMode(CacheMode.Ignore);
        }

        public void Evict(object o)
        {
            session.Evict(o);
        }

        public void Refresh(object o)
        {
            session.Refresh(o);
        }


        public virtual void Dispose()
        {
            session.Dispose();
        }
    }

    public class DAOTransaction : DAOSession, IDAOTransaction
    {
        private readonly ITransaction transaction;
        private readonly List<IDAOValidator> validators;

        public DAOTransaction(ISession session, IUnityContainer parentCfg, List<IDAOValidator> validators)
            : base(session, parentCfg)
        {
            transaction = session.BeginTransaction();
            this.validators = validators;
        }

        public void Put(object Obj)
        {
            foreach (var validator in validators)
                validator.Validate(Obj);

            session.SaveOrUpdate(Obj);
        }

        public bool Contains(object obj)
        {
            return session.Contains(obj);
        }

        public void Insert(object obj)
        {
            foreach (var validator in validators)
                validator.Validate(obj);

            session.Save(obj);
        }

        public void Update(object obj)
        {
            foreach (var validator in validators)
                validator.Validate(obj);

            session.Update(obj);
        }

        public void Delete(object obj)
        {
            session.Delete(obj);
        }

        public void Flush()
        {
            session.Flush();            
        }

        public void Commit()
        {
            session.Flush();
            transaction.Commit();
        }

        public void RollBack()
        {
            session.Clear();
            transaction.Rollback();
        }
    }

    class FactoryConfig
    {
        public ISessionFactory SessionFactory { get; private set; }
        public List<IDAOValidator> Validators { get; private set; }

        public FactoryConfig(ISessionFactory sf)
        {
            SessionFactory = sf;
            Validators = new List<IDAOValidator>();
        }

        public ISession OpenSession()
        {
            if (++InstanceCounter > 20)
            {
                SessionFactory.EvictQueries();
                GC.Collect(GC.MaxGeneration, GCCollectionMode.Optimized);
                InstanceCounter = 0;
                log.Debug("SessionFactory.EvictQueries()");
            }
            return SessionFactory.OpenSession();
        }

        private int InstanceCounter;
        private static readonly log4net.ILog log = log4net.LogManager.GetLogger(
             MethodBase.GetCurrentMethod().DeclaringType);
    }

    /// <summary>
    /// Мысли:
    ///   Надо сделать так, чтобы сессии нормально работали (в случае WinForms - по экземпляру сессии для потока, в случае ASP - своя сессия на каждый запрос
    ///   При этом SessionFactory должен быть один на приложение (точнее их может быть несколько для каждого экземпляра базы с которой работаем)
    /// Получается, что нужен хитрый класс, который для каждой связки провайдер-база содержит свой SessionFactory.
    /// Живет такая фигня в классе SessionFactoryHolder
    /// </summary>
    public abstract class DAOProvider<TProvider> : IDAOProvider
        where TProvider : IDAOProvider, new()
    {
        [Dependency]
        public IUnityContainer Cfg { get; set; }

        /// <summary>
        /// Вызывается после OpenSession, чтобы проверить, можно ли работать с подключенным соединением.
        /// Специально сделан абстрактным, чтобы никто не забыл проверить BASE_CONST где надо
        /// </summary>
        public abstract void CheckBaseConfig(ISession session);

        public virtual void InitValidators(List<IDAOValidator> validators, ISession session)
        {
        }

        protected Assembly mappingAssembly;
        protected String configResourceName;
        protected bool showSQL;

        protected abstract string connectionConfig { get; }

        protected DAOProvider()
        {
            SessionStorage = SessionStorageFactory.GetSessionStorage(Name);
        }

        #region SessionFactory 
        /// <summary>
        /// Тут храняться все когда-либо созданные SessionFactory
        /// Как 
        /// </summary>
        private static readonly Dictionary<string, FactoryConfig> factories = new Dictionary<string, FactoryConfig>();

        internal ISessionStorage SessionStorage;

        public String Name
        {
            get { return String.Format("{0}[{1}]", GetType(), ConnectionString); }
        }

        private FactoryConfig FactoryConfig
        {
            get
            {
                lock (factories)
                {
                    var nm = Name;
                    FactoryConfig cfg;

                    if (factories.TryGetValue(nm, out cfg))
                        return cfg;

                    cfg = new FactoryConfig(InitSessionFactory());

                    using (var session = cfg.OpenSession())
                    {
                        CheckBaseConfig(session);
                        InitValidators(cfg.Validators, session); 
                    }


                    factories.Add(nm, cfg);
                    return cfg;
                }
            }
        }

        protected virtual ISessionFactory InitSessionFactory()
        {
            var cfg = new NHibernate.Cfg.Configuration();
            cfg.Configure(mappingAssembly, configResourceName);
            cfg.Properties["connection.connection_string"] = ConnectionString;

            cfg.Properties["show_sql"] = showSQL.ToString();

            HbmSerializer.Default.Validate = true;

            cfg.AddInputStream(
                HbmSerializer.Default.Serialize(
                    mappingAssembly));

            var sessionFactory = cfg.BuildSessionFactory();

            return sessionFactory;
        }

        #endregion

        public IStatistics SessionFactoryStatictic()
        {
            return FactoryConfig.SessionFactory.Statistics;
        }

        public virtual String ConnectionString
        {
            get { return ConfigReader.GetConnectionString(connectionConfig); }
        }

        public void Evict(Type persistentObjType, object objId)
        {
            if (objId == null)
                FactoryConfig.SessionFactory.Evict(persistentObjType);
            else
                FactoryConfig.SessionFactory.Evict(persistentObjType, objId);
        }

        public IDAOSession StartRead
        {
            get
            {
                var session = FactoryConfig.OpenSession();
                session.FlushMode = FlushMode.Never;
                return new DAOSession(session, Cfg);
            }
        }

        public IDAOTransaction StartTransaction
        {
            get
            {
                var factoryCfg = FactoryConfig;
                return new DAOTransaction(factoryCfg.OpenSession(), Cfg, factoryCfg.Validators);
            }
        }

        public static T InTransaction<T>(Func<T> what, TimeSpan? ScopeTimeout = null)
        {
            bool ok = false;
            var h = DAOTransactionHolder.GetHolder(new List<IDAOProvider> { new TProvider() }, false, ScopeTimeout);
            try
            {
                T result = what();
                ok = true;
                return result;
            }
            finally
            {
                DAOTransactionHolder.PutHolder(h, ok);
            }
        }

        public static void InTransaction(Action what, TimeSpan? ScopeTimeout = null)
        {
            bool ok = false;
            var h = DAOTransactionHolder.GetHolder(new List<IDAOProvider> { new TProvider() }, false, ScopeTimeout);
            try
            {
                what();
                ok = true;
            }
            finally
            {
                DAOTransactionHolder.PutHolder(h, ok);
            }
        }

        public static IDAOTransaction Current
        {
            get { return DAOTransactionHolder.GetSession(typeof(TProvider)); }
        }
    }

    public static class ConfigReader
    {
        public static string GetConnectionString(string key)
        {
            foreach (ConnectionStringSettings cs in ConfigurationManager.ConnectionStrings)
            {
                if (cs.Name == key)
                {
                    return cs.ConnectionString;
                }
            }
            throw new ApplicationException("Не указана строка подключения для конфигурации " + key);
        }
    }
}